-
-
Notifications
You must be signed in to change notification settings - Fork 345
Add video_contact_sheet: Generate contact sheets from videos using keyframes #426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces a new video processing utility called video_contact_sheet
that generates visual contact sheets (filmstrip grids) from video files using scene-change keyframe detection. The tool is designed for video dataset QA, content review, and automated preview generation.
- Core functionality for keyframe extraction using histogram difference and contact sheet composition
- CLI interface with multi-threading support for batch processing
- Utility functions for FFmpeg metadata extraction and parallel processing
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
File | Description |
---|---|
video_contact_sheet/utils.py | FFmpeg metadata extraction and thread pool utilities |
video_contact_sheet/requirements.txt | Project dependencies specification |
video_contact_sheet/core.py | Core keyframe extraction and contact sheet generation logic |
video_contact_sheet/cli.py | Command-line interface with click framework |
video_contact_sheet/init.py | Package initialization and version definition |
video_contact_sheet/README.md | Documentation and usage examples |
proc = subprocess.run( | ||
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True | ||
) | ||
return json.loads(proc.stdout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The subprocess.run call lacks proper error handling. When check=True, subprocess.CalledProcessError will be raised but not handled, making it difficult for users to understand what went wrong with ffprobe execution.
proc = subprocess.run( | |
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True | |
) | |
return json.loads(proc.stdout) | |
try: | |
proc = subprocess.run( | |
FFPROBE_CMD + [str(path)], capture_output=True, text=True, check=True | |
) | |
return json.loads(proc.stdout) | |
except subprocess.CalledProcessError as e: | |
raise RuntimeError( | |
f"ffprobe command failed for path {path}. Error: {e.stderr.strip()}" | |
) from e |
Copilot uses AI. Check for mistakes.
@@ -0,0 +1,115 @@ | |||
""" | |||
Core logic: Keyframe extraction, contact table splicing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The term 'contact table splicing' should be 'contact sheet composition' to match the actual functionality and be consistent with terminology used elsewhere in the codebase.
Core logic: Keyframe extraction, contact table splicing. | |
Core logic: Keyframe extraction, contact sheet composition. |
Copilot uses AI. Check for mistakes.
|
||
def _collect_meta(video_path: Path) -> Dict: | ||
info = ffprobe_metadata(video_path) | ||
v_stream = next(s for s in info["streams"] if s["codec_type"] == "video") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The next() function will raise StopIteration if no video stream is found. This should use next() with a default value or proper exception handling to avoid crashes when processing files without video streams.
v_stream = next(s for s in info["streams"] if s["codec_type"] == "video") | |
v_stream = next((s for s in info["streams"] if s["codec_type"] == "video"), None) | |
if v_stream is None: | |
raise ValueError(f"No video stream found in file: {video_path}") |
Copilot uses AI. Check for mistakes.
Scene-change detection for “interesting” keyframes | ||
Multithreaded extraction using OpenCV + ffmpeg | ||
Footer shows duration / resolution / codec | ||
Pure-Python, works on Windows/Linux/macOS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The markdown formatting is missing proper bullet points. Lines 15-18 should be formatted as a proper markdown list with '- ' prefix for each feature.
Scene-change detection for “interesting” keyframes | |
Multithreaded extraction using OpenCV + ffmpeg | |
Footer shows duration / resolution / codec | |
Pure-Python, works on Windows/Linux/macOS | |
- Scene-change detection for “interesting” keyframes | |
- Multithreaded extraction using OpenCV + ffmpeg | |
- Footer shows duration / resolution / codec | |
- Pure-Python, works on Windows/Linux/macOS |
Copilot uses AI. Check for mistakes.
FONT = ImageFont.load_default() | ||
|
||
|
||
def make_contact_sheet( | ||
frames: List[np.ndarray], | ||
metadata: Dict, | ||
cols: int = 5, | ||
margin: int = 8, | ||
) -> Image.Image: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The global font variable should be initialized inside the function or made configurable. Global state can cause issues in multi-threaded environments and makes the code less flexible.
FONT = ImageFont.load_default() | |
def make_contact_sheet( | |
frames: List[np.ndarray], | |
metadata: Dict, | |
cols: int = 5, | |
margin: int = 8, | |
) -> Image.Image: | |
def make_contact_sheet( | |
frames: List[np.ndarray], | |
metadata: Dict, | |
cols: int = 5, | |
margin: int = 8, | |
font: ImageFont.ImageFont = None, | |
) -> Image.Image: | |
if font is None: | |
font = ImageFont.load_default() |
Copilot uses AI. Check for mistakes.
vids = [] | ||
for p in inputs: | ||
if p.is_dir(): | ||
vids.extend(list(p.rglob("*.mp4"))) | ||
else: | ||
vids.append(p) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The video discovery is hardcoded to only find .mp4 files. Consider supporting multiple video formats or making the file extensions configurable to handle common video formats like .avi, .mov, .mkv, etc.
vids = [] | |
for p in inputs: | |
if p.is_dir(): | |
vids.extend(list(p.rglob("*.mp4"))) | |
else: | |
vids.append(p) | |
# Define supported video extensions | |
supported_extensions = [".mp4", ".avi", ".mov", ".mkv"] | |
vids = [] | |
for p in inputs: | |
if p.is_dir(): | |
for ext in supported_extensions: | |
vids.extend(list(p.rglob(f"*{ext}"))) | |
else: | |
if p.suffix in supported_extensions: | |
vids.append(p) |
Copilot uses AI. Check for mistakes.
🚀 Add video_contact_sheet: Generate contact sheets from videos using keyframes
Summary
This PR introduces a new advanced utility script video_contact_sheet which generates visual contact sheets from video files using scene-change keyframes.
Description
This tool extracts keyframes based on histogram difference, composes them into a grid with metadata (duration, resolution, codec), and exports them as JPEG images. It is particularly useful for dataset QA, content review, or automated preview generation.
The script supports both single-file and batch directory input, and includes multi-threaded processing for high efficiency. Full CLI interface and pytest-based test cases are included.
The changes are as follows:
Checks
in the repository
in the PR
Related issue links